package se.devoteam.nexus.ldap;

import java.util.HashSet;
import java.util.Set;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;

import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.jsecurity.realm.ldap.LdapContextFactory;
import org.jsecurity.realm.ldap.LdapUtils;
import org.sonatype.jsecurity.locators.users.PlexusUser;
import org.sonatype.jsecurity.locators.users.PlexusUserLocator;
import org.sonatype.jsecurity.locators.users.PlexusUserSearchCriteria;

/**
 * <code>PlexusUserLocator</code> implementation for the LDAP realm.
 * 
 * @author anders.hammar
 */
@Component(role = PlexusUserLocator.class, hint = "LDAP", description = "LDAP Users")
public class LdapUserLocator extends AbstractLogEnabled implements PlexusUserLocator {

	private static final String SOURCE = "LDAP";
	
    @Requirement
	private LdapConfigurationManager configuration;

    @Requirement
	private LdapContextFactory ctxFactory;

	public String getSource() {
		return SOURCE;
	}
	
	public PlexusUser getUser(String userId) {
		if (getLogger().isDebugEnabled()) {
			getLogger().debug("Retrieving LDAP user '" + userId + "'");
		}

		LdapContext ctx = null;
		try {
			// Get the base for the search
			String searchBase = this.configuration.getUserBaseDn();

			// Create the LDAP search filter
			String searchFilterExpr = this.configuration.getSearchUserFilterExpr();
			Object[] searchFilerArgs = {userId};
			if (getLogger().isDebugEnabled()) {
				getLogger().debug("LDAP user search filter: " + searchFilterExpr);
			}

			// Create the search controls
			SearchControls searchCtls = new SearchControls();
		
			// Specify the search scope
			int searchScope = this.configuration.getUserSearchScope();
			searchCtls.setSearchScope(searchScope);
	
			// Specify the attributes to return
			String userIdNameAttrId = this.configuration.getUserIdAttribute();
			String nameNameAttrId = this.configuration.getNameAttribute();
			String emailAddressNameAttrId = this.configuration.getEmailAddressAttribute();
			String searchAttributes[]= {userIdNameAttrId, nameNameAttrId, emailAddressNameAttrId};
			searchCtls.setReturningAttributes(searchAttributes);
			
			// Search for objects using the search base, the search filter, and the search controls
			ctx = this.ctxFactory.getSystemLdapContext();
			NamingEnumeration<SearchResult> answers = ctx.search(searchBase, searchFilterExpr, searchFilerArgs, searchCtls);

			PlexusUser user;
			if (answers.hasMore()) {
				SearchResult tmpAnswer = answers.next();
				Attributes attributes = tmpAnswer.getAttributes();
				user = convertToPlexusUser(attributes);
				getLogger().debug("User object retrieved through LDAP");
			} else {
				user = null;
				getLogger().debug("LDAP user not found");
			}
			return user;
		} catch (NamingException e) {
			getLogger().error("Error while retrieving LDAP user", e);
			throw new RuntimeException("Error while retrieving LDAP user", e);
		} finally {
			LdapUtils.closeContext(ctx);
		}
	}

	/**
	 * Always returns <code>true</code>, regardless of the realm is enabled or not.<br>
	 * The method will be removed in future versions of Nexus.
	 */
	public boolean isPrimary() {
		return true;
	}

	public Set<String> listUserIds() {
		getLogger().debug("Retrieving list of LDAP user IDs");

		LdapContext ctx = null;
		try {
			// Get the base for the search
			String searchBase = this.configuration.getUserBaseDn();

			// Create the LDAP search filter
			String searchFilter = this.configuration.getListUsersFilterExpr();
			if (getLogger().isDebugEnabled()) {
				getLogger().debug("LDAP user IDs search filter: " + searchFilter);
			}

			// Create the search controls
			SearchControls searchCtls = new SearchControls();
		
			// Specify the search scope
			int searchScope = this.configuration.getUserSearchScope();
			searchCtls.setSearchScope(searchScope);
	
			// Specify the attributes to return
			String userIdNameAttrId = this.configuration.getUserIdAttribute();
			String searchAttributes[]= {userIdNameAttrId};
			searchCtls.setReturningAttributes(searchAttributes);
			
			// Search for objects using the search base, the search filter, and the search controls
			ctx = this.ctxFactory.getSystemLdapContext();
			NamingEnumeration<SearchResult> answers = ctx.search(searchBase, searchFilter, searchCtls);

			Set<String> userIds = new HashSet<String>();
			while (answers.hasMore()) {
				SearchResult tmpAnswer = answers.next();
				Attributes attributes = tmpAnswer.getAttributes();
				Attribute attribute = attributes.get(userIdNameAttrId);
				userIds.add((String) attribute.get());
			}
	
			if (getLogger().isDebugEnabled()) {
				getLogger().debug("List of LDAP user IDs retrieved (" + userIds.size() + " users)");
			}
			return userIds;
		} catch (NamingException e) {
			getLogger().error("Error while retrieving list of LDAP user IDs", e);
			throw new RuntimeException("Error while retrieving list of LDAP user IDs", e);
		} finally {
			LdapUtils.closeContext(ctx);
		}
	}

	public Set<PlexusUser> listUsers() {
		getLogger().debug("Retrieving list of LDAP users");

		LdapContext ctx = null;
		try {
			// Get the base for the search
			String searchBase = this.configuration.getUserBaseDn();

			// Create the LDAP search filter
			String searchFilter = this.configuration.getListUsersFilterExpr();
			if (getLogger().isDebugEnabled()) {
				getLogger().debug("LDAP user IDs search filter: " + searchFilter);
			}

			// Create the search controls
			SearchControls searchCtls = new SearchControls();
		
			// Specify the search scope
			int searchScope = this.configuration.getUserSearchScope();
			searchCtls.setSearchScope(searchScope);
	
			// Specify the attributes to return
			String userIdNameAttrId = this.configuration.getUserIdAttribute();
			String nameNameAttrId = this.configuration.getNameAttribute();
			String emailAddressNameAttrId = this.configuration.getEmailAddressAttribute();
			String searchAttributes[]= {userIdNameAttrId, nameNameAttrId, emailAddressNameAttrId};
			searchCtls.setReturningAttributes(searchAttributes);
			
			// Search for objects using the search base, the search filter, and the search controls
			ctx = this.ctxFactory.getSystemLdapContext();
			NamingEnumeration<SearchResult> answers = ctx.search(searchBase, searchFilter, searchCtls);

			Set<PlexusUser> users = new HashSet<PlexusUser>();
			while (answers.hasMore()) {
				SearchResult tmpAnswer = answers.next();
				Attributes attributes = tmpAnswer.getAttributes();
				users.add(convertToPlexusUser(attributes));
			}
	
			if (getLogger().isDebugEnabled()) {
				getLogger().debug("List of LDAP users retrieved (" + users.size() + " users)");
			}
			return users;
		} catch (NamingException e) {
			getLogger().error("Error while retrieving list of LDAP users", e);
			throw new RuntimeException("Error while retrieving list of LDAP users", e);
		} finally {
			LdapUtils.closeContext(ctx);
		}
	}

	public Set<PlexusUser> searchUsers(PlexusUserSearchCriteria criteria) {
		if (getLogger().isDebugEnabled()) {
			getLogger().debug("Searching for LDAP users (criteria: " + getLogString(criteria) + ")");
		}

		LdapContext ctx = null;
		try {
			// Get the base for the search
			String searchBase = this.configuration.getUserBaseDn();

			// Create the LDAP search filter
			String searchFilterExpr = this.configuration.getSearchUsersFilterExpr();
			Object[] searchFilerArgs = {criteria.getUserId()};
			if (getLogger().isDebugEnabled()) {
				getLogger().debug("LDAP users search filter: " + searchFilterExpr);
			}

			// Create the search controls
			SearchControls searchCtls = new SearchControls();
		
			// Specify the search scope
			int searchScope = this.configuration.getUserSearchScope();
			searchCtls.setSearchScope(searchScope);
	
			// Specify the attributes to return
			String userIdNameAttrId = this.configuration.getUserIdAttribute();
			String nameNameAttrId = this.configuration.getNameAttribute();
			String emailAddressNameAttrId = this.configuration.getEmailAddressAttribute();
			String searchAttributes[]= {userIdNameAttrId, nameNameAttrId, emailAddressNameAttrId};
			searchCtls.setReturningAttributes(searchAttributes);
			
			// Search for objects using the search base, the search filter, and the search controls
			ctx = this.ctxFactory.getSystemLdapContext();
			NamingEnumeration<SearchResult> answers = ctx.search(searchBase, searchFilterExpr, searchFilerArgs, searchCtls);

			Set<PlexusUser> users = new HashSet<PlexusUser>();
			while (answers.hasMore()) {
				SearchResult tmpAnswer = answers.next();
				Attributes attributes = tmpAnswer.getAttributes();
				users.add(convertToPlexusUser(attributes));
			}
	
			if (getLogger().isDebugEnabled()) {
				getLogger().debug("List of LDAP users retrieved (" + users.size() + " users)");
			}
			return users;
		} catch (NamingException e) {
			getLogger().error("Error while searching LDAP users", e);
			throw new RuntimeException("Error while searching LDAP users", e);
		} finally {
			LdapUtils.closeContext(ctx);
		}
	}
	
	private PlexusUser convertToPlexusUser(Attributes attributes) throws NamingException {
		String userIdNameAttrId = this.configuration.getUserIdAttribute();
		String nameNameAttrId = this.configuration.getNameAttribute();
		String emailAddressNameAttrId = this.configuration.getEmailAddressAttribute();

		PlexusUser user = new PlexusUser();
		Attribute userId = attributes.get(userIdNameAttrId);
		user.setUserId((String) userId.get());
		Attribute name = attributes.get(nameNameAttrId);
		user.setName((String) name.get());
		Attribute emailAddress = attributes.get(emailAddressNameAttrId);
		user.setEmailAddress((String) emailAddress.get());
		user.setSource(SOURCE);
		
		return user;
	}
	
	private String getLogString(PlexusUserSearchCriteria criteria) {
		StringBuilder str = new StringBuilder();
		str.append("userId=");
		str.append(criteria.getUserId());
		str.append(",oneOfRolesIds=");
		str.append(criteria.getOneOfRoleIds());
		
		return str.toString();
	}
}
